﻿using Nop.Core;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Logistics;
using Nop.Core.Domain.Shipping;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Nop.Services.Logistics
{
    abstract class PackageState : IPackageHandle
    {
        #region Fields

        protected LogisticsPackage _package;
        protected LogisticsPlan _plan;
        protected ILogisticsService _logisticsService;
        protected Orders.IOrderProcessingService _orderProcessingService;
        protected Shipment _boundShipment;
        protected int _currentCustomerId;
        protected bool _canManage;

        protected List<ShippingStatus> nextStatuses = new List<ShippingStatus>();

        #endregion

        #region Utilities

        protected static int GetStopsToAction(LogisticsLocation location)
        {
            return location.Plan.AssignedLocations
                .Where(l => l.Rank.HasValue &&
                    l.Rank.Value <= location.Rank &&
                    l.Id != location.Id)
                .Select(l => l.AddressId)
                .Distinct()
                .Count();
        }

        protected static bool IsFinalDelivery(LogisticsPackage package, ILogisticsService service)
        {
            return service.IsMaxDeliveryAttempt(package);
        }

        protected virtual bool IsCommissionStatus(ShippingStatus status)
        {
            //TODO: refactor
            if (_package.Status == ShippingStatus.ReadyForPickUp)
            {
                if (status == ShippingStatus.Delivering ||
                    status == ShippingStatus.ReturningWarehouse)
                {
                    return true;
                }
            }
            else if (_package.Status == ShippingStatus.Delivering)
            {
                if (status == ShippingStatus.Delivered)
                {
                    return true;
                }
            }

            return false;
        }

        protected virtual void UpdatePackage(bool validForCommission = false, PackageNoteTemplate template = null, int? addressId = null, bool adminNote = false, params object[] args)
        {
            _package.UpdatedOnUtc = DateTime.UtcNow;
            _logisticsService.UpdatePackage(_package, false);

            string noteContent;
            if (template == null)
            {
                noteContent = _logisticsService.GetPackageNoteContentByStatus(_package.Status, _package.Shipment.Order.CustomerLanguageId);
            }
            else
            {
                noteContent = _logisticsService.GetPackageNoteContentByTemplate(template, _package.Shipment.Order.CustomerLanguageId);
            }

            if (String.IsNullOrEmpty(noteContent))
            {
                noteContent = "Status is updated."; // should not hit here
            }
            else if (args != null && args.Length > 0)
            {
                noteContent = String.Format(noteContent, args[0]);
            }

            var packageNote = new PackageNote()
            {
                PackageId = _package.Id,
                CustomerId = _currentCustomerId,
                Note = noteContent,
                //only for reschedule delivery date
                Type = adminNote ? PackageNoteType.AdminNote : PackageNoteType.SystemEvent,
                DisplayToCustomer = true,
                CreatedOnUtc = DateTime.UtcNow,
                ShippingStatus = _package.Status,
                AddressId = addressId
            };

            if (_plan != null)
            {
                _logisticsService.CreatePlanNote(
                    PlanNoteStatus.UpdatePackage,
                    _plan.Id,
                    _plan.Status,
                    _currentCustomerId, validForCommission, true,
                    _package.Id,
                    _package.Status, null, null);
            }

            _logisticsService.CreatePackageNote(packageNote);
        }

        protected virtual void StateError(string message)
        {
            throw new NopException("{0}, ({1})", message, ToString());
        }

        #endregion

        #region Abstract methods

        public abstract bool CanCancel();

        public abstract bool CanChangePickupAddress();

        public abstract bool CanChangeDeliveryAddress();

        public abstract bool CanDeliver();

        #endregion

        #region Methods

        protected PackageState()
        {

        }

        public static IPackageHandle CreatePackageState(
            LogisticsPackage package,
            ILogisticsService service,
            Orders.IOrderProcessingService orderService,
            Shipment boundShipment,
            LogisticsInformationSettings settings,
            int currentCustomerId, bool canManage = false)
        {
            PackageState state = null;
            LogisticsPlan plan = null;
            int stopsToLockStatus = settings.LeadStopsToLockStatus == 0 ? 3 : settings.LeadStopsToLockStatus;

            if ((package.Status == ShippingStatus.ReadyForPickUp || package.Status == ShippingStatus.NotYetReady)
                && (!package.PickUpLocation.PlanId.HasValue && !package.DeliveryLocation.PlanId.HasValue))
            {
                state = new PendingState();
            }
            else if ((package.Status == ShippingStatus.ReadyForPickUp)
                && (package.PickUpLocation.PlanId.HasValue && package.PickUpLocation.Plan.Status != PlanStatus.Closed)
                && (!package.DeliveryLocation.PlanId.HasValue || package.DeliveryLocation.PlanId == package.PickUpLocation.PlanId))
            {
                plan = package.PickUpLocation.Plan;

                if (GetStopsToAction(package.PickUpLocation) <= stopsToLockStatus)
                {
                    state = new OnCollectionState();
                }
                else
                {
                    state = new PreCollectionState();
                }

                if (package.DeliveryLocation.PlanId == package.PickUpLocation.PlanId)
                {
                    state.nextStatuses.Add(ShippingStatus.Delivering);
                }
                else
                {
                    state.nextStatuses.Add(ShippingStatus.ReturningWarehouse);
                }
                state.nextStatuses.Add(ShippingStatus.FailedPickUp);
            }
            else if ((package.Status == ShippingStatus.ReturningWarehouse || package.Status == ShippingStatus.FailedPickUp)
                && (package.PickUpLocation.PlanId.HasValue && package.PickUpLocation.Plan.Status != PlanStatus.Closed)
                && (!package.DeliveryLocation.PlanId.HasValue || package.DeliveryLocation.PlanId == package.PickUpLocation.PlanId))
            {
                plan = package.PickUpLocation.Plan;

                state = new PostCollectionState();

                if (canManage && package.Status == ShippingStatus.ReturningWarehouse)
                    state.nextStatuses.Add(ShippingStatus.InWarehouse);
            }
            else if ((package.Status == ShippingStatus.ReturningWarehouse)
                && (package.DeliveryLocation.PlanId.HasValue && package.DeliveryLocation.Plan.Status != PlanStatus.Closed)
                && (!package.PickUpLocation.PlanId.HasValue || package.DeliveryLocation.PlanId == package.PickUpLocation.PlanId))
            {
                plan = package.DeliveryLocation.Plan;
                state = new CancelDeliveryState();

                if (canManage && package.Status == ShippingStatus.ReturningWarehouse)
                    state.nextStatuses.Add(ShippingStatus.InWarehouse);
            }
            else if (package.Status == ShippingStatus.InWarehouse)
            // We don't check the plan status coz the package may be in process of arriving the wharehouse.
            {
                if (!package.DeliveryLocation.PlanId.HasValue && (!package.PickUpLocation.PlanId.HasValue || package.PickUpLocation.Plan.Status == PlanStatus.Closed))
                {
                    plan = null;

                    state = new PreservedState();
                }
                else if (package.DeliveryLocation.PlanId.HasValue && package.DeliveryLocation.Plan.Status == PlanStatus.Running)
                {
                    plan = package.DeliveryLocation.Plan;
                    state = new PreDeliveryState();
                    state.nextStatuses.Add(ShippingStatus.Delivering);
                }
            }
            else if (package.Status == ShippingStatus.InWarehouseFailedDelivery)
            {
                if ((!package.DeliveryLocation.PlanId.HasValue && (!package.PickUpLocation.PlanId.HasValue || package.PickUpLocation.Plan.Status == PlanStatus.Closed)) ||
                    (package.DeliveryLocation.PlanId.HasValue && package.DeliveryLocation.Plan.Status == PlanStatus.Closed)
                    )
                {
                    plan = null;

                    if (IsFinalDelivery(package, service))
                    {
                        state = new SealedState();
                    }
                    else
                    {
                        state = new PreservedState();
                    }
                }
                else if (package.DeliveryLocation.PlanId.HasValue && package.DeliveryLocation.Plan.Status == PlanStatus.Running)
                {
                    plan = package.DeliveryLocation.Plan;
                    state = new PreDeliveryState();
                    state.nextStatuses.Add(ShippingStatus.Delivering);
                }
            }
            else if ((package.Status == ShippingStatus.Delivering)
                && (!package.PickUpLocation.PlanId.HasValue || package.PickUpLocation.Plan.Status == PlanStatus.Closed || package.DeliveryLocation.PlanId == package.PickUpLocation.PlanId)
                && package.DeliveryLocation.PlanId.HasValue)
            {
                plan = package.DeliveryLocation.Plan;

                if (GetStopsToAction(package.DeliveryLocation) <= stopsToLockStatus)
                {
                    state = new OnDeliveryState();
                }
                else
                {
                    state = new PreDeliveryState();
                }

                state.nextStatuses.Add(ShippingStatus.Delivered);
                state.nextStatuses.Add(ShippingStatus.ReturningWarehouseFailedDelivery);
            }
            else if ((package.Status == ShippingStatus.ReturningWarehouseFailedDelivery)
                && (!package.PickUpLocation.PlanId.HasValue || package.PickUpLocation.Plan.Status == PlanStatus.Closed || package.DeliveryLocation.PlanId == package.PickUpLocation.PlanId)
                && package.DeliveryLocation.PlanId.HasValue)
            {
                plan = package.DeliveryLocation.Plan;

                state = new PostDeliveryState();

                if (canManage)
                    state.nextStatuses.Add(ShippingStatus.InWarehouseFailedDelivery);
            }
            else if (package.Status == ShippingStatus.FailedPickUp || package.Status == ShippingStatus.Delivered || package.Status == ShippingStatus.Cancelled)
            {
                plan = null;

                state = new CompletedState();
            }

            if (state == null)
            {
                plan = package.PickUpLocation.PlanId.HasValue ? package.PickUpLocation.Plan : package.DeliveryLocation.Plan;
                state = new LockedState();
            }

            state._plan = plan;
            state._package = package;
            state._logisticsService = service;
            state._orderProcessingService = orderService;
            state._boundShipment = boundShipment;
            state._currentCustomerId = currentCustomerId;
            state._canManage = canManage;

            return state;
        }

        public virtual bool CanUpdate()
        {
            return _plan == null || _plan.Status == PlanStatus.Running;
        }

        public virtual void Cancel()
        {
            if (!CanCancel())
                StateError("Cannot cancel in this status.");

            _package.Status = ShippingStatus.Cancelled;
            _package.PickUpLocation.PlanId = null;
            _package.PickUpLocation.Rank = null;
            _package.DeliveryLocation.PlanId = null;
            _package.DeliveryLocation.Rank = null;

            UpdatePackage();
        }

        protected virtual void OnChangeAddress()
        {
            if (_package.Status != ShippingStatus.NotYetReady &&
                _package.Status != ShippingStatus.ReadyForPickUp &&
                _package.Status != ShippingStatus.InWarehouse &&
                _package.Status != ShippingStatus.InWarehouseFailedDelivery
                )
                StateError("Cannot remove the package from the plan.");

            _package.PickUpLocation.PlanId = null;
            _package.PickUpLocation.Rank = null;
            _package.DeliveryLocation.PlanId = null;
            _package.DeliveryLocation.Rank = null;
        }

        public virtual void ChangePickupAddress(int addressId)
        {
            if (!CanChangePickupAddress())
                StateError("Cannot change the pickup address in this status.");

            var previousAddressId = _package.PickUpLocation.AddressId;
            OnChangeAddress();

            var addressService = Nop.Core.Infrastructure.EngineContext.Current.Resolve<Nop.Services.Common.IAddressService>();
            var address = addressService.GetAddressById(addressId);
            if (address == null)
                throw new ArgumentNullException("addressId");

            _package.PickUpLocation.Address = address;

            UpdatePackage(false, PackageNoteStandardTemplates.PickupAddressChanged, previousAddressId);
        }

        public virtual bool IsChangeDeliveryAddressFree()
        {
            return _logisticsService.GetFailedDeliveryEventCount(_package) == 0 && _package.DeliveryAddressChangeCount == 0;
        }

        public virtual void ChangeDeliveryAddress(int addressId)
        {
            if (!CanChangeDeliveryAddress())
                StateError("Cannot change the delivery address in this status.");

            var previousAddressId = _package.DeliveryLocation.AddressId;
            OnChangeAddress();

            var addressService = Nop.Core.Infrastructure.EngineContext.Current.Resolve<Nop.Services.Common.IAddressService>();
            var address = addressService.GetAddressById(addressId);
            if (address == null)
                throw new ArgumentNullException("addressId");

            _package.DeliveryLocation.Address = address;
            _package.DeliveryAttemptCount = 0; // Reset delivery attempt counter
            _package.DeliveryAddressChangeCount += 1;

            UpdatePackage(false, PackageNoteStandardTemplates.DeliveryAddressChanged, previousAddressId);
        }

        public virtual void FailDelivery(PackageNoteTemplate failedReasonTemplate)
        {
            if (!CanDeliver())
                StateError("Cannot fail delivery in this status.");

            _package.DeliveryAttemptCount++;
            _package.Status = ShippingStatus.ReturningWarehouseFailedDelivery;
            UpdatePackage(false, failedReasonTemplate);
        }

        public virtual List<ShippingStatus> GetNextStatuses()
        {
            return nextStatuses;
        }

        public virtual void UpdateStatus(ShippingStatus status)
        {
            if (!GetNextStatuses().Contains(status))
                StateError(String.Format("Cannot update to {0} in this status.", status));

            //TODO: refactor - put in states
            if (status == ShippingStatus.InWarehouse || status == ShippingStatus.InWarehouseFailedDelivery)
            {
                if (_package.ToWarehouseId.HasValue)
                {
                    _package.Warehouse = _package.ToWarehouse;

                }
                else
                {
                    _package.Warehouse = _plan.Warehouse;
                }
                _package.DeliveryLocation.Rank = null;

                // Set the package deliveryLocation's Rank to null.
                // This is important for district-to-district transfer to make sure plan
                // auto-close when all packages changed by courier to In-HuB
                _package.ToWarehouse = null;
            }
            else if (_package.Status != ShippingStatus.ReadyForPickUp &&
                _package.Status != ShippingStatus.FailedPickUp &&
                _package.Status != ShippingStatus.InWarehouse &&
                _package.Status != ShippingStatus.InWarehouseFailedDelivery &&
                _package.Status != ShippingStatus.Delivered &&
                _package.Status != ShippingStatus.Cancelled)
            {
                _package.Warehouse = null;
            }

            var validCommission = IsCommissionStatus(status);
            _package.Status = status;

            UpdatePackage(validCommission);
        }

        public virtual string GetStateString()
        {
            return String.Format("ID: {0}, PickupPlan: {1}, DeliveryPlan: {2}, Status {3}.",
                    _package.Id, _package.PickUpLocation.PlanId ?? 0, _package.DeliveryLocation.PlanId ?? 0, _package.Status);
        }

        //schedule delivery date only for sealed state and PostDeliveryState
        public virtual bool CanScheduleDelivery()
        {
            return false;
        }

        public virtual bool IsScheduleDeliveryFree()
        {
            return _package.DeliveryRescheduleCount == 0 && !IsFinalDelivery(_package, _logisticsService);
        }

        public virtual void ScheduleDelivery(DateTime date)
        {
            _package.DeliveryAttemptCount = 0;
            _package.DeliveryRescheduleCount += 1;
            UpdatePackage(false, PackageNoteStandardTemplates.DeliveryDateChanged, null, true, date.ToString("d"));
        }

        public virtual void WarehousePickUp(int warehouseId)
        {
            string name = _logisticsService.GetWarehouseById(warehouseId).Name;
            UpdatePackage(false, PackageNoteStandardTemplates.WarehousePickUp, null, true, name);
        }
        #endregion
    }

    #region States

    class PendingState : PackageState
    {
        public override bool CanCancel()
        {
            return true;
        }

        public override bool CanChangePickupAddress()
        {
            return true;
        }

        //change delivery address shoulw work after successfully pick up
        public override bool CanChangeDeliveryAddress()
        {
            return false;
        }

        public override bool CanDeliver()
        {
            return false;
        }
    }

    class PreCollectionState : PackageState
    {
        public override bool CanCancel()
        {
            return true;
        }

        public override bool CanChangePickupAddress()
        {
            return true;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return false;
        }

        public override bool CanDeliver()
        {
            return false;
        }

        public override void UpdateStatus(ShippingStatus status)
        {
            // TODO: refactor
            if (_package.Shipment.Order.ShippingStatus == ShippingStatus.NotYetShipped ||
                _package.Shipment.Order.ShippingStatus == ShippingStatus.ReadyForPickUp ||
                _package.Shipment.Order.ShippingStatus == ShippingStatus.PartiallyShipped)
            {
                if (status == ShippingStatus.ReturningWarehouse || status == ShippingStatus.Delivering)
                {
                    _orderProcessingService.Ship(_package.Shipment, true);

                    if (_boundShipment != null)
                        _orderProcessingService.Ship(_boundShipment, true);
                }

                if (status == ShippingStatus.FailedPickUp)
                {
                    //TODO: how to deal with the bound shipment
                    _orderProcessingService.FailedPickup(_package.Shipment, true);
                }
            }

            base.UpdateStatus(status);
        }
    }

    class OnCollectionState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return true;
        }

        public override bool CanDeliver()
        {
            return false;
        }

        public override void UpdateStatus(ShippingStatus status)
        {
            // TODO: refactor
            if (_package.Shipment.Order.ShippingStatus == ShippingStatus.NotYetShipped ||
                _package.Shipment.Order.ShippingStatus == ShippingStatus.ReadyForPickUp ||
                _package.Shipment.Order.ShippingStatus == ShippingStatus.PartiallyShipped)
            {
                if (status == ShippingStatus.ReturningWarehouse || status == ShippingStatus.Delivering)
                {
                    _orderProcessingService.Ship(_package.Shipment, true);

                    if (_boundShipment != null)
                        _orderProcessingService.Ship(_boundShipment, true);
                }

                if (status == ShippingStatus.FailedPickUp)
                {
                    //TODO: how to deal with the bound shipment
                    _orderProcessingService.FailedPickup(_package.Shipment, true);
                }
            }

            base.UpdateStatus(status);
        }
    }

    class PostCollectionState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        //not for fail pick up
        public override bool CanChangeDeliveryAddress()
        {
            return false;
        }

        public override bool CanDeliver()
        {
            return false;
        }

        protected override void OnChangeAddress()
        {
            if (!_package.PickUpLocation.PlanId.HasValue)
                StateError("Package is not in a pickup plan when changing address.");

            _package.DeliveryLocation.Rank = null;
            _package.DeliveryLocation.PlanId = null;
            _package.Status = ShippingStatus.ReturningWarehouse;
        }
    }

    class PreservedState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return true;
        }

        public override bool CanDeliver()
        {
            return false;
        }
    }

    class PreDeliveryState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return true;
        }

        public override bool CanDeliver()
        {
            return true;
        }

        protected override void OnChangeAddress()
        {
            if (!_package.DeliveryLocation.PlanId.HasValue)
                StateError("Package is not in a delivery plan when changing address.");

            _package.PickUpLocation.Rank = _package.DeliveryLocation.Rank;
            _package.PickUpLocation.PlanId = _package.DeliveryLocation.PlanId;
            _package.DeliveryLocation.Rank = null;
            _package.DeliveryLocation.PlanId = null;
            _package.Status = ShippingStatus.ReturningWarehouse;
        }

        public override void UpdateStatus(ShippingStatus status)
        {
            // TODO: refactor
            if (status == ShippingStatus.Delivered && _package.Shipment.Order.ShippingStatus != ShippingStatus.Delivered)
            {
                _orderProcessingService.Deliver(_package.Shipment, true);

                if (_boundShipment != null)
                    _orderProcessingService.Deliver(_boundShipment, true);
            }

            base.UpdateStatus(status);
        }
    }

    class CancelDeliveryState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return true;
        }

        public override bool CanDeliver()
        {
            return false;
        }
    }

    class OnDeliveryState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return false;
        }

        public override bool CanDeliver()
        {
            return true;
        }

        public override void UpdateStatus(ShippingStatus status)
        {
            // TODO: refactor
            if (status == ShippingStatus.Delivered && _package.Shipment.Order.ShippingStatus != ShippingStatus.Delivered)
            {
                _orderProcessingService.Deliver(_package.Shipment, true);

                if (_boundShipment != null)
                    _orderProcessingService.Deliver(_boundShipment, true);
            }

            base.UpdateStatus(status);
        }
    }

    class PostDeliveryState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return true;
        }

        public override bool CanDeliver()
        {
            return false;
        }

        public override bool CanScheduleDelivery()
        {
            return true;
        }

        protected override void OnChangeAddress()
        {
        }
    }

    class SealedState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return true;
        }

        public override bool CanDeliver()
        {
            return false;
        }

        public override bool CanScheduleDelivery()
        {
            return true;
        }
    }

    class CompletedState : PackageState
    {
        public override bool CanCancel()
        {
            return (_package.Status == ShippingStatus.ReadyForPickUp || _package.Status == ShippingStatus.NotYetReady ||
                       _package.Status == ShippingStatus.NotYetShipped);
        }

        public override bool CanChangePickupAddress()
        {
            return (_package.Status == ShippingStatus.ReadyForPickUp || _package.Status == ShippingStatus.NotYetReady ||
                       _package.Status == ShippingStatus.NotYetShipped);
        }

        public override bool CanChangeDeliveryAddress()
        {
            return false;
        }

        public override bool CanDeliver()
        {
            return false;
        }
    }

    class LockedState : PackageState
    {
        public override bool CanCancel()
        {
            return false;
        }

        public override bool CanChangePickupAddress()
        {
            return false;
        }

        public override bool CanChangeDeliveryAddress()
        {
            return false;
        }

        public override bool CanDeliver()
        {
            return false;
        }
    }

    #endregion
}
